本章内容:通过之前几章的学习,我们学到了大多数游戏所需的东西,你现在完全可以做一些小游戏了。比如FC游戏小蜜蜂(Galaxian)。或者坦克大战等等。但是,现在还不太好玩,因为画面比较生硬,而且没有声音,现在,我们就开始介绍如何做出动画效果及播放声音。
动画与音效
动画
动画能够很好的提高画面的可视性,当然,动画不单单是一个单纯的贴图替换的过程,也可能包含图片颜色的改变,位置的改变,形状的改变等等。就平时常用的而言,动画分为下面几种:帧动画,骨骼动画,粒子效果,mesh变形等等。他们并没有一个完全严格意义的概念划分,只是我们在使用时的人为划分。
帧动画
帧动画是2d游戏中最常用的动画形式,因为它简单易用,基本不涉及什么计算,画面表现丰富。当然,它也有相应的缺点,比如贴图占据空间大,越流畅、精细的画面,占据内存空间越大,绘制比较麻烦,因为每一帧几乎等于重绘。
帧动画还有一种变体,是将动画的各个部分划分为小的动画,比如头部,手臂等,然后把他们通过固定的位置拼接在一起。这种好处是可以替换某个部分来实现换装备,换武器或者一个类型的动画可以组装多种人物等等。
骨骼动画
骨骼动画是比较复杂的动画,但是它有很多优点,比如可以复用贴图,不用画很多帧,方便控制,可以结合mesh动画等。目前比较流行的有spine和dragonbone两个骨骼动画编辑器。其中spine有对应love2d的运行时,所以一般使用骨骼动画都用spine,不过spine本身是收费的,而且不便宜。骨骼动画的原理是将一个人物分为若干块骨头,我们主要控制骨头的长短,角度,位置等,然后骨头上绑定贴图,就可以了,所以最简单的骨骼动画记录的是随时间变化各个骨骼的位置和角度。
刚刚提到的帧动画变体实际上有时也可以加入骨骼动画,只不过一般不太设计关节角度的继承关系,而是直接用绝对位置和绝对角度。因为他们的编辑比较复杂,或者没有一个统一的定式,这里不多讲,后面可以针对性的自己试着实现。
粒子效果
实际上,粒子效果本身不太算动画部分,但是因为他们也涉及贴图,位置,角度,大小等的变化,所以放在这里将。粒子效果实际上是一大批的小图片,按照预先设计好的存在时间,位置,速度,角度,加速度,颜色等等(很多有随机成分),加入到场景,并当存在时间到时时自动消失。我们常用见的比如,魔法效果,火星,灰尘,闪光,火焰等,均可由粒子效果来模拟。对了,加一句计算机图形的名言“这个东西看起来是真的,那它就是真的”。
mesh动画
也可以叫做mesh的动态形变。如果知道些绘图原理或者3d渲染的同学,应该理解什么是mesh,它是计算机渲染图片的形式,计算机会把欲绘制的位置与纹理的位置做一个对应关系,一般至少要3个点来完成。一个方形的贴图一般需要4个点,即两个三角形来完成渲染。而mesh动画则是把一个贴图布满mesh网格,每个单元都是三角形。然后通过控制这些单元的顶点位置来达到变形效果。这种变形往往比较有张力,而且立体。比如一个软球,一个飘动的旗子,一些透视形变等等。这种效果一般也会结合骨骼动画来实现,比如spine。所以有时,spine能做出以假乱真的3d效果。
本章仅具体介绍一下帧动画的原理和库,骨骼动画可以到spine的运行时的github上找到案例(去找一个叫love2d的branch),粒子效果可以找一些粒子工具来体验一下感觉,其实所有的参数都是通用的。mesh动画就还是找spine运行时。
帧动画原理及实现。
基本原理
帧动画的基本远离实际上跟电影胶片差不多,就是利用人的视觉残像原理,以及人脑自动补全的原理的,通过快速的切换一些画面的显示来达到某个图片看起来像在动一样。
帧动画一般涉及几个要素,一个是帧序列,就是一组图片。一个是延迟时间,帧与帧之间的延迟间隔。还有一个是循环模式,比如单次,顺序循环,乒乓循环几种。
多图帧动画
多图是比较简单的方式,每个图片是已经切割好的纹理,我们按正常的方法建立一组image对象,然后通过一个timer来控制播放即可。下面通过代码演示:
上面的代码比较简单,唯一需要注意的是引入image对象时,对文件名的处理,可以用string.format进行格式化。
spritesheet帧动画
精灵表帧动画的原理实际跟上面是没有区别的,只是由于如果图片不是二次幂(自己百度)的话,它实际占用的显存要高于图片本身。所以一般把图片整理后放到一个大图片中,这个图片叫做精灵清单,压制这种图片的工具比较多,比较著名的是spritepacker,额,也是个收费工具。在love论坛中有对其文件格式解读的工具,由于笔者并不用这个软件,所以请自行查阅。另外,在网上还有很多并没有给出具体拆分方式的精灵清单。如果他们的间隔是固定的,(比如一些人做的是64*64大小,每个精灵之间再额外间隔2个像素),这种就比较容易处理。不过有素材,可能是从某些游戏导出的,原本是存在一个清单的,但是导出时没能找到,这种情况就需要手动切图,在重新排布了。常用的有比如photoshop,graphicgale等。
从代码实现上,与上面的代码的区别在于需要引入love的另一个对象叫做quad。它可以用来作为draw的第二个参数,从而告诉draw函数要绘制目标的哪个矩形区域。
quad在定义时,需要x,y,w,h参数,另外还需要一个图片整体大小。需要说明的是,像素的起点是0,0 而非1,1 所以宽100,高100的图片,实际x的取值范围是0,99。
所以有下面代码:
帧动画库
常用的帧动画库anim8,它建立动画分为两个步骤,首先建立一个网格,然后通过网格建立动画,具体用法可以自行github。
另外,笔者也自己写了一个简单的animation库,可以在笔者github上找到。
其中参数img为图片对象,fx,fy为含有动画的第一帧左上角位置,w,h为每个帧的尺寸,offx,offy为帧之间的间隔,lx,ly为最后一帧右下角的位置,delay为帧延迟时间,count为取这个动画中的前多少帧(可以缺省为最多)。这个库包含一些基本的回调。因为很简单,所以自己看源码就看得懂。
声音
在love2d里,声音对象是由love.audio.newSource来导入的,因为很简单下面仅举个例子。
有两点要说明,newSource的参数的第二个为类型,有两种,一种是静态一种是流式,前者比较快,但占内存,后者相反。缺省为流式。如果像类似机枪那种连续的播放某个声音的话,需要建立多个声音对象,如果仅一个的话,只能在这个声音完全播完,才能再次播放。
编程时间
这次我们的坦克再次升级啦,改为飞机了,现在给坦克披上飞机的皮,它就是飞机了,不过炮塔也没有了,用鼠标来操控飞机的方向。
设计阶段
- 飞机能够跟随鼠标的方向转动,并自动向前飞行,飞机有转动速度。
- 飞机能够按下按键时发射子弹。
- 飞机绑定一个动画。
- 设计一个子弹类,子弹在射出时固定角度,按固定速度移动,离开图像边界子弹销毁。
- 设计一个敌人飞机类,并绑定一个动画,敌人飞机自动发射子弹,但敌人飞机的子弹速度要慢一点。
- 敌人的初始位置为以画面为中心,半径rx = 400,ry =300的随机外围位置。并以画面中心为目标飞行。
- 敌人飞机在超过边界时,销毁并重新生成一个飞机。
- 暂时不涉及碰撞。留作作业。
根据上述设计要求,我们首先确定了制作3个类,一个玩家,一个敌人,一个子弹。动画部分我们准备使用anim8库。
玩家跟随鼠标的算法,我们需要考虑下如何实现,敌人生成的位置还是比较简单的。
实现阶段
- 三个类的基本框架,我们在上一章已经讲过了,这里不再赘述。(实际上,敌人飞机虽然像飞机,但是它跟子弹在代码上更加相似。所以,有时候对于游戏来讲,要从一个物体的行为本质思考,而不是外面的皮肤是什么样的)
- 飞机类跟随鼠标的算法,这里简单讲解一下。先把代码贴出来,再简要的说明:1234567891011121314151617181920212223local function getRot(x1,y1,x2,y2)if x1==x2 and y1==y2 then return 0 endlocal angle=math.atan((x2-x1)/(y2-y1))if y1-y2<0 then angle=angle-math.pi endif angle>0 then angle=angle-2*math.pi endreturn -angleendlocal function unitAngle(angle) --convert angle to 0,2*Piangle = angle%(2*math.pi)if angle > math.pi then angle = angle - 2* math.pi endreturn angleendlocal tx,ty = love.mouse.getPosition()local rot = unitAngle(getRot(self.x,self.y,tx,ty)) --这里是计算方位角的一种方法。self.rot = unitAngle(self.rot)if rot>self.rot and math.abs(rot - self.rot)< math.pi orrot< self.rot and math.abs(rot - self.rot)> math.pi thenself.rot = self.rot + self.drelseself.rot = self.rot - self.drend
首先,获取方位角的公式,上一章已经讲过了,数学方法不说了,当黑箱使用。返回的是两点间方位角。
第二个函数是获得单位角度,因为我们的角度不断叠加,可能不在-Pi~Pi(因为这个区间段比较容易跟0进行比较)的范围内,但是由于三角函数都是周期函数,所以从外观上没有什么影响,但是如果进行加减或比较,就要出问题了,所以要进行归一化。同样,数学方法不再解释了。
然后我们到判断转向部分,self.dr是飞机的转动速度,首先我们得到鼠标与飞机的方位角rot,然后跟飞机当前的角度进行比较,如果方位角大于飞机的角又小于半圈(math.pi)则右转,或者小于飞机角度,但大于半圈(这种情况实际是飞机处于正方向的两端),否则左转。(图示我下次修订教程的时候再补).
- 关于敌人飞机发射子弹以及子弹减速的问题。1234local b = Bullet(self)b.vx = b.vx/3b.vy = b.vy/3table.insert(game.objects,b)
在敌人发射子弹时,生成一个子弹实例,实际上跟玩家是一样的。要减速,则要改变子弹实例的速度(而非类的速度,否则所有子弹均被改变,不能改变模板),这里注意的是,直接设置speed属性是有问题的,因为后面子弹update里根本没有涉及speed而是vx,vy,所以要改变它们,让他们各自打3折即可。
- 敌人飞机生成位置1234local rot = love.math.random()*2*math.piself.x = math.sin(rot)*400 + 400self.y = -math.cos(rot)*300 + 300self.rot = rot+math.pi
如何随机的在一个圈的位置上生成位置?同样是数学问题,不想多说了,代码在上面,因为rot是指向外圈的,所以用rot+pi就是反向指向内圈。(注意不是-rot,-rot的意义是沿0对称,而非反向,周期性不解释)
关于anim8的用法
请自己参阅anim8的文档,这里不再赘述。动态生成/删除飞机
我们之前说过了,在某个对象遍历过程中,增添或删除对象是十分危险的,因为有序表很可能排序被打乱。一般而言,有两种解决方案,我们任选。当然,如果你是无序表就无所谓了,但是也不能用insert或remove来控制,直接nil掉就可以了。
第一种方法,我叫做另起炉灶123456new = {}for i ,v in ipairs(objects)v:update() --table.insert(new,newObj)插入到new中,而非当前if not v.destroyed then table.insert(new,v) endendobjects = new
第二种方法,是一种变通的方法。
当然,还有一种方法就是不在遍历中加入,而是放到后面。
上面介绍了本章节中比较复杂的代码,其他的东西实际上是一样的。只是代码的量增加了。
作业
- 增加些碰撞测试吧。让敌人及其子弹对玩家有碰撞,一旦碰撞就gameover了。(使用bump库很简单啦)
- 增添一些敌人的种类,子弹的方向等。自定义速度,子弹方向,及子弹速度,颜色等。(需要稍微改一下bullet类,以便支持更多的自定义)
- 把这个游戏改编一下,变成常见的纵版弹幕的样式,即飞机随机从屏幕上方飞往下方,发射子弹。玩家鼠标控制飞机移动,方向永远对着屏幕上方。然后设计几种不同的武器类型,比如横向的,散弹的,追踪的(算法跟用鼠标控制飞机方向类似)等等。
本章代码
|
|